import { PostgreSqlContainer, StartedPostgreSqlContainer } from '@testcontainers/postgresql';
import { GenericContainer, getContainerRuntimeClient, Network, StartedNetwork, StartedTestContainer } from "testcontainers";
import { ChildProcessWithoutNullStreams, spawn } from 'child_process';
import { get } from 'http';

export type StartServerOption = {
  dbOptions: Array<{
    database: 'postgres'
    sqlpath: string
  }>;
};

export class ServerManager {
  serverType: 'local' | 'container' = process.env.CI ? 'container' : 'local';
  private dbContainer?: StartedPostgreSqlContainer;
  private appContainer?: StartedTestContainer;
  private network?: StartedNetwork;
  private localAppProcess?: ChildProcessWithoutNullStreams;
  private localAppPort: number = 0;


  async startServer(options: StartServerOption): Promise<{ appUrl: string }> {
    this.network = await new Network().start();
    this.dbContainer = await new PostgreSqlContainer(
      "xa-dd3-rancher-master.grapecity.net/apollo/postgres:13.3-alpine"
    )
      .withPassword('your_strong_password')
      .withAutoRemove(true)
      .withCopyFilesToContainer([{
        source: options.dbOptions[0].sqlpath,
        target: '/docker-entrypoint-initdb.d/import.sql'
      }])
      .withNetwork(this.network)
      .withNetworkAliases("postgres")
      .start();
    const dbUsername = this.dbContainer.getUsername();
    const dbPassword = this.dbContainer.getPassword();

    let appUrl: string;
    let dbConnectionUrl: string;
    if (this.serverType === 'container') {
      dbConnectionUrl = `jdbc:postgresql://postgres:5432/${this.dbContainer.getDatabase()}`;
    } else {
      dbConnectionUrl = `jdbc:postgresql://localhost:${this.dbContainer.getMappedPort(5432)}/${this.dbContainer.getDatabase()}`;
    }

    if (this.serverType === 'container') {
      this.appContainer = await new GenericContainer('xa-dd3-rancher-master.grapecity.net/apollo/todo')
        .withEnvironment({
          "quarkus.datasource.jdbc.url": dbConnectionUrl,
          "quarkus.datasource.username": dbUsername,
          "quarkus.datasource.password": dbPassword
        })
        .withExposedPorts(8080)
        .withAutoRemove(true)
        .withNetwork(this.network)
        .start();

      appUrl = `http://localhost:${this.appContainer.getMappedPort(8080)}/todo`;
      await this.waitForServerToBeReady(appUrl, 10, 5000);
    } else {
      await this.startLocalApp();
      appUrl = `http://localhost:${this.localAppPort}/todo`;
      await this.waitForServerToBeReady(appUrl, 10, 30000);
    }
    return { appUrl };
  }

  /**
   * Checks if the server is alive by making HTTP requests
   * @param url The URL to check
   * @param intervalMs The interval between checks in milliseconds
   * @param timeoutMs The timeout in milliseconds
   */
  private async waitForServerToBeReady(url: string, intervalMs: number, timeoutMs: number): Promise<void> {
    const startTime = Date.now();

    return new Promise<void>((resolve, reject) => {
      const checkServer = () => {
        if (Date.now() - startTime > timeoutMs) {
          return reject(new Error(`Server did not start within ${timeoutMs}ms timeout`));
        }

        get(url, (res) => {
          if (res.statusCode && res.statusCode >= 200 && res.statusCode < 400) {
            console.log(`Server is ready at ${url} with status code ${res.statusCode}`);
            resolve();
          } else {
            setTimeout(checkServer, intervalMs);
          }
        }).on('error', (err) => {
          setTimeout(checkServer, intervalMs);
        });
      };

      checkServer();
    });
  }

  async restoreDb(): Promise<{ appUrl: string }> {
    if (this.serverType === 'container') {
      await this.appContainer?.stop({
        remove: false,
        removeVolumes: false
      });
    } else {
      this.localAppProcess?.kill('SIGKILL');
    }
    await this.dbContainer?.restoreSnapshot('init');
    let appUrl: string;
    if (this.serverType === 'container') {
      if (this.appContainer) {
        await this.appContainer.restart();
        appUrl = `http://localhost:${this.appContainer.getMappedPort(8080)}/todo`;
        await this.waitForServerToBeReady(appUrl, 10, 5000);
      } else {
        throw new Error('App container not found');
      }
    } else {
      await this.startLocalApp();
      appUrl = `http://localhost:${this.localAppPort}/todo`;
      await this.waitForServerToBeReady(appUrl, 10, 30000);
    }
    return { appUrl };
  }

  async stopServer(): Promise<void> {
    if (this.serverType === 'container') {
      await this.appContainer?.stop();
    } else if (this.localAppProcess) {
      this.localAppProcess.kill();
    }
    await this.dbContainer?.stop();
    await this.network?.stop();
  }

  private async startLocalApp(): Promise<void> {
    const jarPath = 'C:/Users/CruzLiu/Downloads/spring-integration-boot-master/todo/build/quarkus-app/quarkus-run.jar';
    const dbUsername = this.dbContainer?.getUsername();
    const dbPassword = this.dbContainer?.getPassword();
    const localDbConnectionUrl = `jdbc:postgresql://localhost:${this.dbContainer?.getMappedPort(5432)}/${this.dbContainer?.getDatabase()}`;

    console.log(`Starting local application with Java...`);
    this.localAppProcess = spawn('java', [
      `-Dquarkus.datasource.jdbc.url=${localDbConnectionUrl}`,
      `-Dquarkus.datasource.username=${dbUsername}`,
      `-Dquarkus.datasource.password=${dbPassword}`,
      `-Dquarkus.http.port=${this.localAppPort}`,
      '-jar',
      jarPath
    ]);

    await new Promise<void>((resolve, reject) => {
      const timeout = setTimeout(() => {
        cleanupAndReject(new Error('Timeout waiting for Java process to report port'));
      }, 30000); // 30 second timeout

      const cleanup = () => {
        if (!this.localAppProcess) return;
        this.localAppProcess.stdout.off('data', onData);
        this.localAppProcess.stderr.off('data', onData);
        this.localAppProcess.off('error', onError);
        this.localAppProcess.off('exit', onExit);
      };

      const cleanupAndResolve = () => {
        clearTimeout(timeout);
        cleanup();
        resolve();
      };
      const cleanupAndReject = (err: Error) => {
        clearTimeout(timeout);
        cleanup();
        reject(err);
      };

      const onData = (data: Buffer) => {
        const output = data.toString();
        const match = output.match(/Listening on:.*:(\d+)/);
        if (match) {
          this.localAppPort = parseInt(match[1], 10);
          cleanupAndResolve();
        }
      };

      const onError = (err: Error) => {
        cleanupAndReject(err);
      };

      const onExit = (code: number, signal: string) => {
        if (this.localAppPort > 0) return;
        cleanupAndReject(new Error(`Java process exited before reporting port. code=${code}, signal=${signal}`));
      };

      if (!this.localAppProcess) {
        return cleanupAndReject(new Error("localAppProcess is not initialized"));
      }
      this.localAppProcess.stdout.on('data', onData);
      this.localAppProcess.stderr.on('data', onData);
      this.localAppProcess.on('error', onError);
      this.localAppProcess.on('exit', onExit);
    });
  }
}
